relationship.go 11 KB

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