relationship.go 12 KB

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