relationship.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package state
  2. import (
  3. "bytes"
  4. "context"
  5. "fmt"
  6. "strings"
  7. "text/template"
  8. )
  9. // relationshipSQLTpl defines the template for a SQL query used to query buddy
  10. // list and privacy relationships between a user (`me`) and other users in the
  11. // system.
  12. //
  13. // This query serves two purposes:
  14. // 1. Retrieve all relationships for the user.
  15. // 2. If filtering is enabled (`.DoFilter` is true), retrieve all relationships
  16. // filtered on a specific list of users.
  17. //
  18. // The query creates a unified view of both server-side buddy lists and
  19. // client-side buddy lists.
  20. const relationshipSQLTpl = `
  21. WITH myScreenName AS (SELECT ?),
  22. {{ if .DoFilter }}filter AS (SELECT * FROM (VALUES%s) as t),{{ end }}
  23. theirBuddyLists AS (SELECT feedbag.screenName AS screenName,
  24. MAX(CASE WHEN feedbag.classId = 0 THEN 1 ELSE 0 END) AS isBuddy,
  25. MAX(CASE WHEN feedbag.classId = 2 THEN 1 ELSE 0 END) AS isPermit,
  26. MAX(CASE WHEN feedbag.classId = 3 THEN 1 ELSE 0 END) AS isDeny
  27. FROM feedbag
  28. WHERE feedbag.name = (SELECT * FROM myScreenName)
  29. {{ if .DoFilter }}AND feedbag.screenName IN (SELECT * FROM filter){{ end }}
  30. AND feedbag.classId IN (0, 2, 3)
  31. AND EXISTS(SELECT 1
  32. FROM buddyListMode
  33. WHERE buddyListMode.screenName = feedbag.screenName
  34. AND useFeedbag IS TRUE)
  35. GROUP BY feedbag.screenName
  36. UNION
  37. SELECT me AS screenName,
  38. isBuddy AS isBuddy,
  39. isPermit AS isPermit,
  40. isDeny AS isDeny
  41. FROM clientSideBuddyList
  42. WHERE them = (SELECT * FROM myScreenName)
  43. {{ if .DoFilter }}AND me IN (SELECT * FROM filter){{ end }}
  44. AND EXISTS(SELECT 1
  45. FROM buddyListMode
  46. WHERE buddyListMode.screenName = clientSideBuddyList.me
  47. AND useFeedbag IS FALSE)),
  48. yourBuddyList AS (SELECT feedbag.name AS screenName,
  49. MAX(CASE WHEN feedbag.classId = 0 THEN 1 ELSE 0 END) AS isBuddy,
  50. MAX(CASE WHEN feedbag.classId = 2 THEN 1 ELSE 0 END) AS isPermit,
  51. MAX(CASE WHEN feedbag.classId = 3 THEN 1 ELSE 0 END) AS isDeny
  52. FROM feedbag
  53. WHERE feedbag.screenName = (SELECT * FROM myScreenName)
  54. {{ if .DoFilter }}AND feedbag.name IN (SELECT * FROM filter){{ end }}
  55. AND feedbag.classId IN (0, 2, 3)
  56. AND EXISTS(SELECT 1
  57. FROM buddyListMode
  58. WHERE buddyListMode.screenName = feedbag.screenName
  59. AND useFeedbag IS TRUE)
  60. GROUP BY feedbag.name
  61. UNION
  62. SELECT them AS screenName,
  63. isBuddy AS isBuddy,
  64. isPermit AS isPermit,
  65. isDeny AS isDeny
  66. FROM clientSideBuddyList
  67. WHERE me = (SELECT * FROM myScreenName)
  68. {{ if .DoFilter }}AND them IN (SELECT * FROM filter){{ end }}
  69. AND EXISTS(SELECT 1
  70. FROM buddyListMode
  71. WHERE buddyListMode.screenName = clientSideBuddyList.me
  72. AND useFeedbag IS FALSE)),
  73. theirPrivacyPrefs AS (SELECT buddyListMode.screenName,
  74. CASE
  75. WHEN buddyListMode.useFeedbag IS TRUE THEN IFNULL(feedbagPrefs.pdMode, 1)
  76. ELSE buddyListMode.clientSidePDMode END AS pdMode
  77. FROM buddyListMode
  78. LEFT JOIN feedbag feedbagPrefs
  79. ON (feedbagPrefs.screenName == buddyListMode.screenName AND
  80. feedbagPrefs.classID = 4)
  81. WHERE EXISTS (SELECT 1
  82. FROM theirBuddyLists
  83. WHERE theirBuddyLists.screenName = buddyListMode.screenName)
  84. OR EXISTS (SELECT 1
  85. FROM yourBuddyList
  86. WHERE yourBuddyList.screenName = buddyListMode.screenName)),
  87. yourPrivacyPrefs AS (SELECT buddyListMode.screenName,
  88. CASE
  89. WHEN buddyListMode.useFeedbag IS TRUE THEN IFNULL(feedbagPrefs.pdMode, 1)
  90. ELSE buddyListMode.clientSidePDMode END AS pdMode
  91. FROM buddyListMode
  92. LEFT JOIN feedbag feedbagPrefs
  93. ON (feedbagPrefs.screenName == buddyListMode.screenName AND
  94. feedbagPrefs.classID = 4)
  95. WHERE buddyListMode.screenName = (SELECT * FROM myScreenName))
  96. SELECT COALESCE(yourBuddyList.screenName, theirBuddyLists.screenName) AS screenName,
  97. CASE
  98. WHEN yourPrivacyPrefs.pdMode = 1 THEN false
  99. WHEN yourPrivacyPrefs.pdMode = 2 THEN true
  100. WHEN yourPrivacyPrefs.pdMode = 3 THEN IFNULL(yourBuddyList.isPermit, false) = false
  101. WHEN yourPrivacyPrefs.pdMode = 4 THEN IFNULL(yourBuddyList.isDeny, false)
  102. WHEN yourPrivacyPrefs.pdMode = 5 THEN IFNULL(yourBuddyList.isBuddy, false) = false
  103. ELSE false
  104. END AS youBlock,
  105. CASE
  106. WHEN theirPrivacyPrefs.pdMode = 1 THEN false
  107. WHEN theirPrivacyPrefs.pdMode = 2 THEN true
  108. WHEN theirPrivacyPrefs.pdMode = 3 THEN IFNULL(theirBuddyLists.isPermit, false) = false
  109. WHEN theirPrivacyPrefs.pdMode = 4 THEN IFNULL(theirBuddyLists.isDeny, false)
  110. WHEN theirPrivacyPrefs.pdMode = 5 THEN IFNULL(theirBuddyLists.isBuddy, false) = false
  111. ELSE false
  112. END AS blocksYou,
  113. IFNULL(theirBuddyLists.isBuddy, false) AS onTheirBuddyList,
  114. IFNULL(yourBuddyList.isBuddy, false) AS onYourBuddyList
  115. FROM theirBuddyLists
  116. FULL OUTER JOIN yourBuddyList
  117. ON (yourBuddyList.screenName = theirBuddyLists.screenName)
  118. JOIN theirPrivacyPrefs
  119. ON (theirPrivacyPrefs.screenName = COALESCE(theirBuddyLists.screenName, yourBuddyList.screenName))
  120. JOIN yourPrivacyPrefs ON (1 = 1)
  121. `
  122. var (
  123. queryWithoutFiltering = tmplMustCompile(struct{ DoFilter bool }{DoFilter: false})
  124. queryWithFiltering = tmplMustCompile(struct{ DoFilter bool }{DoFilter: true})
  125. )
  126. // Relationship represents the relationship between two users.
  127. // Users A and B are related if:
  128. // - A has user B on their buddy list, or vice versa
  129. // - A has user B on their deny list, or vice versa
  130. // - A has user B on their permit list, or vice versa
  131. type Relationship struct {
  132. // User is the screen name of the user with whom you have a relationship.
  133. User IdentScreenName
  134. // BlocksYou indicates whether user blocks you. This is true when user has
  135. // the following permit/deny modes set:
  136. // - DenyAll
  137. // - PermitSome (and you are not on permit list)
  138. // - DenySome (and you are on deny list)
  139. // - PermitOnList (and you are not on their buddy list)
  140. BlocksYou bool
  141. // YouBlock indicates whether you block user. This is true when user has
  142. // the following permit/deny modes set:
  143. // - DenyAll
  144. // - PermitSome (and they are not on your permit list)
  145. // - DenySome (and they are on your deny list)
  146. // - PermitOnList (and they are not on your buddy list)
  147. YouBlock bool
  148. // IsOnTheirList indicates whether you are on user's buddy list.
  149. IsOnTheirList bool
  150. // IsOnYourList indicates whether this user is on your buddy list.
  151. IsOnYourList bool
  152. }
  153. // Relationship retrieves the relationship between the specified user (`me`)
  154. // and another user (`them`).
  155. //
  156. // This method always returns a usable [Relationship] value. If the user
  157. // specified by `them` does not exist, the returned [Relationship] will have
  158. // default boolean values.
  159. func (f SQLiteUserStore) Relationship(ctx context.Context, me IdentScreenName, them IdentScreenName) (Relationship, error) {
  160. rels, err := f.AllRelationships(ctx, me, []IdentScreenName{them})
  161. if err != nil {
  162. return Relationship{}, fmt.Errorf("error getting relationships: %w", err)
  163. }
  164. if len(rels) == 0 {
  165. return Relationship{
  166. User: them,
  167. }, nil
  168. }
  169. return rels[0], nil
  170. }
  171. // AllRelationships retrieves the relationships between the specified user (`me`)
  172. // and other users.
  173. //
  174. // A relationship is defined by the [Relationship] type, which describes the nature
  175. // of the connection between users.
  176. //
  177. // This function only includes users who have activated their buddy list through
  178. // a call to [SQLiteUserStore.RegisterBuddyList]. The results can be optionally
  179. // filtered to include only specific users by providing their identifiers in
  180. // the `filter` parameter.
  181. func (f SQLiteUserStore) AllRelationships(ctx context.Context, me IdentScreenName, filter []IdentScreenName) ([]Relationship, error) {
  182. tpl := queryWithoutFiltering
  183. args := make([]any, 1, len(filter)+1)
  184. args[0] = me.String()
  185. if len(filter) > 0 {
  186. // add placeholders to template
  187. placeholders := strings.TrimRight(strings.Repeat("(?),", len(filter)), ",")
  188. tpl = fmt.Sprintf(queryWithFiltering, placeholders)
  189. // assemble arguments to match placeholders
  190. for _, sn := range filter {
  191. args = append(args, sn.String())
  192. }
  193. }
  194. rows, err := f.db.QueryContext(ctx, tpl, args...)
  195. if err != nil {
  196. return nil, fmt.Errorf("error querying relationships: %w", err)
  197. }
  198. defer func() {
  199. _ = rows.Close()
  200. }()
  201. var relationships []Relationship
  202. for rows.Next() {
  203. var screenName string
  204. rel := Relationship{}
  205. err = rows.Scan(
  206. &screenName,
  207. &rel.YouBlock,
  208. &rel.BlocksYou,
  209. &rel.IsOnTheirList,
  210. &rel.IsOnYourList,
  211. )
  212. if err != nil {
  213. return nil, fmt.Errorf("error scanning row: %w", err)
  214. }
  215. rel.User = NewIdentScreenName(screenName)
  216. relationships = append(relationships, rel)
  217. }
  218. if err = rows.Err(); err != nil {
  219. return nil, fmt.Errorf("error iterating over relationship rows: %w", err)
  220. }
  221. return relationships, nil
  222. }
  223. func tmplMustCompile(data any) string {
  224. tmpl, err := template.New("").Parse(relationshipSQLTpl)
  225. if err != nil {
  226. panic(err)
  227. }
  228. buf := &bytes.Buffer{}
  229. err = tmpl.Execute(buf, data)
  230. if err != nil {
  231. panic(err)
  232. }
  233. return buf.String()
  234. }